########################################################################
#  Wiizard - A Wii games manager
#  Copyright (C) 2023  CYBERDEViL
#
#  This file is part of Wiizard.
#
#  Wiizard is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Wiizard is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################


import pickle

from PyQt5.QtWidgets import (
  QApplication,
  QWidget,
  QListView,
  QSizePolicy,
  QLabel,
  QPushButton,
  QSpacerItem,
  QHBoxLayout,
  QVBoxLayout,
  QAbstractItemView,
  QStyle,
  QFrame,
  QComboBox,
  QMenu,
  QToolBar,
  QStatusBar
)
from PyQt5.QtCore import (
  Qt,
  pyqtSignal,
  QMimeData,
  QUrl,
  QTimer
)
from PyQt5.QtGui import (
  QGuiApplication,
  QDesktopServices,
  QDrag,
  QPainter
)

import wiizard.globals as Global
from wiizard.models.source import (
  SourceSelecterModel,
  SourceListModel,
  SOURCE_TYPE_LOCAL
  #, SOURCE_TYPE_DEVICE, SOURCE_TYPE_FILEPART
)
from wiizard.views.repair import RepairDialog
from wiizard.widgets.diskUsage import DiskUsageWidget
from wiizard.translations import _


class GamesViewMessage(QWidget):
  """ When a message (with optional button) needs to be displayed in
      'GamesView', a instance of this class will be set as 'GamesView' it's
      viewport.
  """
  def __init__(self, message, buttonText=None, parent=None):
    QWidget.__init__(self, parent=parent)
    layout = QVBoxLayout(self)

    spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
    layout.addItem(spacer)

    label = QLabel(message, self)
    label.setWordWrap(True)
    label.setAlignment(Qt.AlignCenter)
    label.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
    layout.addWidget(label)

    if buttonText is not None:
      self.button = QPushButton(buttonText, self)
      self.button.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
      layout.addWidget(self.button)
      layout.setAlignment(self.button, Qt.AlignHCenter)

    spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
    layout.addItem(spacer)


class InWidgetMessage(QWidget):
  def __init__(self, timeout=3, parent=None):
    QWidget.__init__(self, parent=parent)
    self.__timer = QTimer(self)
    layout = QHBoxLayout(self)
    self.__icon  = QLabel(self)
    self.__label = QLabel(self)
    self.__label.setWordWrap(True)
    layout.addWidget(self.__icon)
    layout.addWidget(self.__label)
    layout.setAlignment(self.__icon, Qt.AlignTop)

    self.__infoPixmap = QApplication.style().standardIcon(QStyle.SP_MessageBoxInformation).pixmap(16, 16)
    self.__warnPixmap = QApplication.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(16, 16)
    self.__errPixmap  = QApplication.style().standardIcon(QStyle.SP_MessageBoxCritical).pixmap(16, 16)

    self.setStyleSheet("background-color: rgba(0, 0, 0, 128); "
                       "border-style: solid; border-radius: 10px; "
                       "border-width: 5px;")
    self.setAttribute(Qt.WA_TranslucentBackground, True)

    self.__timer.setInterval(timeout * 1000)
    self.__timer.setSingleShot(True)
    self.__timer.timeout.connect(self.hide)

    self.raise_()
    self.hide()

  def __show(self, message):
    self.setMaximumWidth(self.parent().width())
    self.__label.setText(message)
    self.adjustSize()
    self.show()
    self.__timer.start()

  def showMessage(self, message):
    self.__icon.setPixmap(self.__infoPixmap)
    self.__show(message)

  def showWarning(self, warning):
    self.__icon.setPixmap(self.__warnPixmap)
    self.__show(warning)

  def showError(self, error):
    self.__icon.setPixmap(self.__errPixmap)
    self.__show(error)

  def mousePressEvent(self, event):
    self.hide()


# https://doc.qt.io/qt-6/qlistview.html
class GamesView(QListView):
  """ The view for disc/games from a source.
  """
  viewModeChanged = pyqtSignal()  # for updating external buttons
  gamesUpdated    = pyqtSignal()  # re-emit from current model (if any)

  def __init__(self, parent=None):
    QListView.__init__(self, parent=parent)

    self.messageWidget = InWidgetMessage(parent=self.parent())

    self.setSelectionMode(QAbstractItemView.ExtendedSelection)
    self.setResizeMode(QListView.Adjust)
    self.setViewMode(QListView.IconMode)
    self.setAlternatingRowColors(False)
    self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
    self.setWordWrap(True)

    # Drag and drop stuff
    self.dragStartPos     = None

    # Backup view mode
    self.__viewMode       = QListView.IconMode

    # Alt view port
    self.__altViewport    = None
    self.setViewport(QWidget(self))
    QListView.setViewMode(self, QListView.ListMode)
    QListView.setViewMode(self, self.__viewMode)
    self.reApplyDND()

  def resetViewport(self):
    if self.__altViewport:
      # @NOTE: setViewport will delete the previous viewport widget for us.
      self.setViewport(QWidget(self))
      self.__altViewport = None

      # Re-apply view mode (else drag and drop won't work)
      # TODO FIXME we first have to apply setViewMode(QListView.ListMode) else
      # drag and drop won't work (atleast on empty list)
      QListView.setViewMode(self, QListView.ListMode)
      QListView.setViewMode(self, self.__viewMode)
      self.reApplyDND()

  def rescan(self):
    sourceModel = self.model().sourceModel
    sourceModel.scanStarted.connect(self.__onStartScan)
    sourceModel.scanCompleted.connect(self.__onScanComplete)
    sourceModel.startScan()

  def __onStartScan(self):
    sourceModel = self.model().sourceModel

    # For now only the scan thread of local type is cancellable
    if sourceModel.type == SOURCE_TYPE_LOCAL:
      self.__altViewport = GamesViewMessage(_("Scanning.. please wait"),
                                            _("Cancel"), parent=self)
      self.__altViewport.button.clicked.connect(self.__onCancelScan)
    else:
      self.__altViewport = GamesViewMessage(_("Scanning.. please wait"),
                                            parent=self)
    self.setViewport(self.__altViewport)

    self.viewport().update()

  def __onCancelScan(self):
    self.model().sourceModel.cancelScan()

  def __onRepair(self):
    dialog = RepairDialog(self.model().sourceModel, parent=self)
    dialog.exec()

  def __onScanComplete(self):
    self.model().sourceModel.scanCompleted.disconnect(self.__onScanComplete)
    self.model().sourceModel.scanStarted.disconnect(self.__onStartScan)

    if self.model().sourceModel.hasErrors():
      self.__altViewport = GamesViewMessage(
        _("Source has {} errors, please repair them first.")
        .format(self.model().sourceModel.hasErrors()), "Repair", self)
      self.setViewport(self.__altViewport)
      self.__altViewport.button.clicked.connect(self.__onRepair)
    else:
      self.resetViewport()

  def setModel(self, model=None):
    # Disconnect previous connection
    curModel = self.model()
    if curModel is not None:
      curModel.sourceModel.gamesUpdated.disconnect(self.gamesUpdated)

    self.resetViewport()
    QListView.setModel(self, model)
    if model is not None:
      model.sourceModel.gamesUpdated.connect(self.gamesUpdated)
      self.rescan()

  def paintEvent(self, event):
    # Alt view is active, don't paint anything.
    if self.__altViewport:
      return

    model = self.model()

    if model is None:
      painter = QPainter(self.viewport())
      painter.setPen(Qt.white)
      painter.drawText(self.rect(), Qt.AlignCenter, _("No source selected"))
      return

    if not model.rowCount():
      painter = QPainter(self.viewport())
      painter.setPen(Qt.white)
      painter.drawText(self.rect(), Qt.AlignCenter, _("No games found"))
      return

    QListView.paintEvent(self, event)

  def dropEvent(self, dropEvent):
    print("Drop!")
    rows         = pickle.loads(dropEvent.mimeData().data("pywbfs/index-list"))
    sourceModel  = dropEvent.source().model()
    sourceGames  = sourceModel.filteredGames
    currentGames = [game.id6 for game in self.model().games]
    for row in rows:
      game = sourceGames[row]
      # skip games that are already present
      if game.id6 in currentGames:
        continue

      self.model().addFutureGame(game)
      print("- ", game.title)

    self.model().modelReset.emit()

  def dragMoveEvent(self, dragMoveEvent):
    dragMoveEvent.accept()

  def dragEnterEvent(self, dragEnterEvent):
    if self.model() is None:
      self.messageWidget.showWarning(_("No model selected, nothing to drop "
                                     "on."))
      return

    if self.model().sourceModel.isScanning():
      self.messageWidget.showWarning(_("Will not drop while scanning."))
      return

    # Don't allow to drop on the same model
    if dragEnterEvent.source().model() == self.model():
      return

    # Non supported Mimetype
    if not dragEnterEvent.mimeData().hasFormat("pywbfs/index-list"):  # TODO change mimetype
      return

    # Don't allow drop when all games are already present
    rows            = pickle.loads(dragEnterEvent.mimeData().data("pywbfs/index-list"))
    sourceModel     = dragEnterEvent.source().model()
    sourceGames     = sourceModel.filteredGames
    currentGames    = [game.id6 for game in self.model().games]
    allGamesPresent = True
    addSize         = 100

    for row in rows:
      game = sourceGames[row]
      if game.id6 not in currentGames:
        allGamesPresent = False
        addSize += game.fileSize

    if allGamesPresent:
      self.messageWidget.showWarning(_("Drop denied: all games are already "
                                     "here."))
      return

    if addSize >= self.model().sourceModel.getFutureFree():
      self.messageWidget.showWarning(_("Drop denied: not enough free space."))
      return

    dragEnterEvent.acceptProposedAction()

  def mousePressEvent(self, mousePressEvent):
    if mousePressEvent.button() == Qt.LeftButton:
      self.dragStartPos = mousePressEvent.pos()
    QListView.mousePressEvent(self, mousePressEvent)

  def mouseMoveEvent(self, mouseMoveEvent):
    if not (mouseMoveEvent.buttons() & Qt.LeftButton):
      return

    if (mouseMoveEvent.pos() - self.dragStartPos).manhattanLength() < QApplication.startDragDistance():
      return

    selectionModel = self.selectionModel()
    if selectionModel is None:
      return
    if not selectionModel.hasSelection():
      return

    # Selected indexes
    indexes = selectionModel.selectedRows()

    # Create drag event
    # https://doc.qt.io/qt-5/dnd.html
    drag = QDrag(self)
    data = QMimeData()
    data.setData("pywbfs/index-list", bytearray(pickle.dumps([index.row() for index in indexes])))
    drag.setMimeData(data)

    # Only 1 selected, use disc image as icon
    if len(indexes) == 1 and Global.SharedGameImages.imgType is not None:
      index   = indexes[0]
      model   = self.model()
      id6Str  = str(model.filteredGames[index.row()].id6)
      icon    = Global.SharedGameImages.getGameImage(id6Str)
      drag.setPixmap(icon)

    drop = drag.exec(Qt.CopyAction)

  def reApplyDND(self):
    self.setAcceptDrops(True)
    self.setDragEnabled(True)
    self.setDefaultDropAction(Qt.IgnoreAction)
    self.setDragDropMode(QAbstractItemView.DragDrop)

  def toggleViewMode(self):
    if self.viewMode() == QListView.IconMode:
      self.setViewMode(QListView.ListMode)
    else:
      self.setViewMode(QListView.IconMode)
    self.update()

  def setViewMode(self, mode):
    self.__viewMode = mode
    QListView.setViewMode(self, mode)
    if mode == QListView.ListMode:
      self.setAlternatingRowColors(True)
      self.reApplyDND()
    else:
      self.setAlternatingRowColors(False)
    self.viewModeChanged.emit()

  def contextMenuEvent(self, contextMenuEvent):
    selectionModel = self.selectionModel()
    if selectionModel is None:
      return

    menu = QMenu(self)
    toggleViewModeAction = menu.addAction(_("Toggle view mode"))

    copyTitleAction    = -1
    copyPathAction     = -1
    copyId6Action      = -1
    markDeletionAction = -1
    delDeletionAction  = -1
    openInFileBrowser  = -1

    if selectionModel.hasSelection():
      copyTitleAction    = menu.addAction(_("Copy title(s)"))
      copyPathAction     = menu.addAction(_("Copy path(s)"))
      copyId6Action      = menu.addAction(_("Copy ID6(s)"))

      markDeletion  = False
      clearDeletion = False

      for index in selectionModel.selectedRows():
        game = self.model().filteredGames[index.row()]
        if self.model().sourceModel.isGameMarkedForDeletion(game):
          clearDeletion = True
        else:
          markDeletion = True

      # Only add "Mark for deletion" when there are games selected that are not
      # yet marked for deletion.
      if markDeletion:
        markDeletionAction = menu.addAction(_("Mark for deletion"))

      # Only add "Clear deletion" when there are games selected that are marked
      # for deletion.
      if clearDeletion:
        delDeletionAction = menu.addAction(_("Clear deletion"))

      # "Open in filebrowser" only available when 1 game is selected
      if len(selectionModel.selectedRows()) == 1:
        openInFileBrowser = menu.addAction(_("Open in filebrowser"))

    # Exec the menu
    action = menu.exec_(self.mapToGlobal(contextMenuEvent.pos()))

    # Handle chosen action
    if action == toggleViewModeAction:
      self.toggleViewMode()

    elif action == copyTitleAction:
      indexes = selectionModel.selectedRows()
      titles  = ""
      for index in indexes:
        titles += self.model().filteredGames[index.row()].title + "\n"
      clipboard = QGuiApplication.clipboard()
      clipboard.setText(titles)

    elif action == copyPathAction:
      indexes = selectionModel.selectedRows()
      paths   = ""
      for index in indexes:
        paths += self.model().filteredGames[index.row()].sourcePath + "\n"
      clipboard = QGuiApplication.clipboard()
      clipboard.setText(paths)

    elif action == copyId6Action:
      indexes = selectionModel.selectedRows()
      id6s    = ""
      for index in indexes:
        id6s += str(self.model().filteredGames[index.row()].id6) + "\n"
      clipboard = QGuiApplication.clipboard()
      clipboard.setText(id6s)

    elif action == markDeletionAction:
      # @NOTE: make sure indexes are sorted desc when deleting stuff
      model   = self.model()
      indexes = [index.row() for index in selectionModel.selectedRows()]
      indexes.sort(reverse=True)
      for index in indexes:
        model.addGameForDeletion(index)

    elif action == delDeletionAction:
      # @NOTE: make sure indexes are sorted desc when deleting stuff
      model   = self.model()
      indexes = [index.row() for index in selectionModel.selectedRows()]
      indexes.sort(reverse=True)
      for index in indexes:
        model.delGameFromDeletion(index)

    elif action == openInFileBrowser:
      index = selectionModel.selectedRows()[0]
      path  = self.model().filteredGames[index.row()].sourcePath
      QDesktopServices.openUrl(QUrl("file://" + path, QUrl.TolerantMode))


class SourceSelecterWidget(QComboBox):
  indexChanged = pyqtSignal(int)  # Not emitted on modelReset unless a selected
                                  # sourceModel got deleted.

  def __init__(self, parent=None):
    QComboBox.__init__(self, parent=parent)

    # Automatic resize width of drop down list
    sizePolicy = self.view().sizePolicy()
    sizePolicy.setHorizontalPolicy(QSizePolicy.MinimumExpanding)
    self.view().setSizePolicy(sizePolicy)

    # Automatic expand width of self
    sizePolicy = self.sizePolicy()
    sizePolicy.setHorizontalPolicy(QSizePolicy.MinimumExpanding)
    self.setSizePolicy(sizePolicy)

    model = SourceSelecterModel()  # TODO there just needs to be 1 instance
    self.setModel(model)

    self.setCurrentIndex(0)  # Placeholder

    self.__currentSelected = None
    self.__modelHasReset   = False

    self.currentIndexChanged.connect(self.__sourceChanged)
    model.modelReset.connect(self.__sourcesUpdated)
    model.preModelReset.connect(self.__preModelReset)

  def __sourceChanged(self, index):
    """ Keep track of current selected model """
    # Model has just reset
    if self.__modelHasReset is True:
      self.__modelHasReset = False
      return

    if index == -1:
      # Set to placeholder
      self.currentIndexChanged.disconnect(self.__sourceChanged)
      self.setCurrentIndex(0)
      self.currentIndexChanged.connect(self.__sourceChanged)
      self.__currentSelected = None
    else:
      # Backup selected item
      self.__currentSelected = self.model().getItem(index)

    self.indexChanged.emit(index)

  def __sourcesUpdated(self):
    """ Set new index of the current selected (if still exists) """
    if self.__currentSelected is not None:
      newIndex = self.model().getIndexForSourceModel(self.__currentSelected)
      self.currentIndexChanged.disconnect(self.__sourceChanged)
      self.setCurrentIndex(newIndex)
      self.__currentSelected = self.model().getItem(newIndex)
      if self.__currentSelected is None:  # The model probably got removed, so emit
        self.indexChanged.emit(newIndex)
      self.currentIndexChanged.connect(self.__sourceChanged)
    else:
      self.__modelHasReset = False

  def __preModelReset(self):
    self.__modelHasReset = True


class SourceWidget(QFrame):
  def __init__(self, parent=None):
    QFrame.__init__(self, parent=parent)

    self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)

    layout = QVBoxLayout(self)

    self.toolbar = QToolBar(self)

    self.sourceCombo    = SourceSelecterWidget(self)
    self.toolbar.addWidget(self.sourceCombo)

    self.sortOrderCombo = QComboBox(self)
    self.sortOrderCombo.addItem(_("Asc"))
    self.sortOrderCombo.addItem(_("Desc"))
    self.sortOrderCombo.setCurrentIndex(0)
    self.sortOrderCombo.setEnabled(False)
    self.sortOrderCombo.setToolTip(_("Sort order"))
    self.toolbar.addWidget(self.sortOrderCombo)

    self.refreshAction  = self.toolbar.addAction(_("Refresh location"))
    self.refreshAction.setToolTip(_("Refresh location"))
    self.refreshAction.setIcon(QApplication.style().standardIcon(QStyle.SP_BrowserReload))
    self.refreshAction.setEnabled(False)

    self.toggleViewAction = self.toolbar.addAction(_("Toggle view mode"))
    self.toggleViewAction.setToolTip("Toggle view mode")
    self.toggleViewAction.setIcon(QApplication.style().standardIcon(QStyle.SP_FileDialogListView))
    self.toggleViewAction.setEnabled(False)

    self.listView = GamesView(self)
    self.listView.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

    self.usageWidget = DiskUsageWidget(parent=self)
    self.statusBar   = QStatusBar(self)

    layout.addWidget(self.toolbar, Qt.AlignTop)
    layout.addWidget(self.listView)
    layout.addWidget(self.usageWidget)
    layout.addWidget(self.statusBar)

    self.sourceCombo.indexChanged.connect(self.__sourceChanged)
    self.sortOrderCombo.currentIndexChanged.connect(self.__onSortOrderChange)
    self.refreshAction.triggered.connect(self.listView.rescan)
    self.toggleViewAction.triggered.connect(self.__onToggleView)
    self.listView.viewModeChanged.connect(self.__onListViewModeChange)

  def __sourceChanged(self, index):
    if self.listView.model() is not None:
      self.listView.model().sourceModel.scanStarted.disconnect(self.__onScanStarted)
      self.listView.model().sourceModel.scanCompleted.disconnect(self.__onScanCompleted)
      self.listView.model().sourceModel.gamesUpdated.disconnect(self.__onGamesUpdated)
      self.listView.model().sourceModel.setInUse(False)

    source = self.sourceCombo.model().getItem(index)
    if source is not None:
      source.setInUse(True)
      model = SourceListModel(source)
      model.setSortOrder(self.sortOrderCombo.currentIndex())

      self.usageWidget.updateSizes(source.getUsed(), source.getCapacity(),
                                   0, source.getFutureFree())
      self.refreshAction.setEnabled(True)
      self.sortOrderCombo.setEnabled(True)
      self.toggleViewAction.setEnabled(True)

      source.scanStarted.connect(self.__onScanStarted)
      source.scanCompleted.connect(self.__onScanCompleted)
      source.gamesUpdated.connect(self.__onGamesUpdated)

      self.listView.setModel(model)
    else:
      self.refreshAction.setEnabled(False)
      self.sortOrderCombo.setEnabled(False)
      self.toggleViewAction.setEnabled(False)
      self.listView.setModel()
      self.usageWidget.updateSizes(-1, -1, -1, -1)

  def __onSortOrderChange(self, index):
    self.listView.model().setSortOrder(index)

  def __onScanStarted(self):
    #self.toolbar.setEnabled(False)  # This didn't do the trick
    self.sourceCombo.setEnabled(False)
    self.refreshAction.setEnabled(False)
    self.sortOrderCombo.setEnabled(False)
    self.toggleViewAction.setEnabled(False)

  def __onScanCompleted(self):
    #self.toolbar.setEnabled(True)  # This didn't do the trick
    self.sourceCombo.setEnabled(True)
    self.refreshAction.setEnabled(True)
    self.sortOrderCombo.setEnabled(True)
    self.toggleViewAction.setEnabled(True)

    source = self.listView.model().sourceModel
    discCount = len(source.games)
    totalSize = 0
    for game in source.games:
      totalSize += game.fileSize
    self.statusBar.showMessage(_(f"{discCount} Discs ({totalSize} MiB)"))

  def __onGamesUpdated(self):
    source = self.listView.model().sourceModel
    self.usageWidget.updateSizes(source.getFutureUsed(),
                                 source.getCapacity(),
                                 source.getFutureUsed(),
                                 source.getFutureFree())  # TODO

  def __onListViewModeChange(self):
    if self.listView.viewMode() == QListView.IconMode:
      self.toggleViewAction.setIcon(
        QApplication.style().standardIcon(QStyle.SP_FileDialogListView)
      )
    else:
      self.toggleViewAction.setIcon(
        QApplication.style().standardIcon(QStyle.SP_FileDialogDetailedView)
      )

  def __onToggleView(self):
    self.listView.toggleViewMode()
